package org.easystub;

import java.util.Map;
import java.util.HashMap;
import java.lang.reflect.Method;

public final class EasyStub {

    private EasyStub() {
    }

    private static final Map<Object, InvocationReporter> stubToReporter = new HashMap<Object, InvocationReporter>();
    private static final Map<Object, RecordedInvocationAnswerer> stubToAnswerer = new HashMap<Object, RecordedInvocationAnswerer>();
    private static final Map<Object, CaptureStore> stubToCaptureStore = new HashMap<Object, CaptureStore>();
    private static final ProxyFactory PROXY_FACTORY = new ProxyFactory();


    /**
     * Creates a stub instance, where all invocations to that stub by default return default values
     * (false for boolean, 0 for number primitives, or null for objects).
     */
    public static <T> T createStub(Class<T> toStub) {
        return createStub(toStub, ReplyType.DEFAULT_VALUES);
    }


    /**
     * Creates a stub instance. Allows you to set the default reply behaviour for method invocations.
     * @see org.easystub.ReplyType
     */
    public static <T> T createStub(Class<T> toStub, ReplyType replyBehaviorType) {
        InvocationHandlerParameters<T> params = new InvocationHandlerParameters<T>(toStub, replyBehaviorType);
        T stub = createProxyInstance(params);
        stubToAnswerer.put(stub, params.getRecordedAnswerer());
        stubToReporter.put(stub, params.getReporter());
        stubToCaptureStore.put(stub, params.getCaptureStore());
        return stub;
    }

    /**
     * Starts recording of method invocation answers. Will return a builder to accept the reply values.
     * @see org.easystub.ResultReporter
     */
    public static <T> ResultReporter<T> stub(T invocation) {
        return createResultReporterForLastInvocation();
    }

    /**
     * used for recording anwers to void methods. Use this when replying with exceptions.
     * @see org.easystub.ResultReporter
     */
    public static ResultReporter<Void> replyToLastMethodCall() {
        return createResultReporterForLastInvocation();
    }

    private static <T> ResultReporter<T> createResultReporterForLastInvocation() {
        Invocation last = GlobalInvocationReporter.lastInvocation();
        RecordedInvocationAnswerer answerer = stubToAnswerer.get(last.getObjectCalled());
        InvocationReporter reporter = stubToReporter.get(last.getObjectCalled());
        return new ResultReporter<T>(reporter, answerer);
    }

    /**
     * Convenience method to allow method parameters to be filled in when recording method invocation answers.
     * This method will actually always return null.
     */
    public static <T> T any(Class<T> argumentClass) {
        return null;
    }

    @SuppressWarnings("unchecked")
    private static <T> T createProxyInstance(InvocationHandlerParameters<T> params) {
        return PROXY_FACTORY.createProxyInstance(params);
    }


    /**
     * Used for registering {@link org.easystub.Capture}s, so a methods parameters can be captured when invocated
     * for later retrieval. Returns a builder object.
     * @see org.easystub.CaptureReporter
     */
    public static CaptureReporter setupCapture(Object invocation) {
        return setupCaptureForLastInvocation();
    }

    /**
     * Used for registering {@link org.easystub.Capture}s, so a methods parameters can be captured when invocated
     * for later retrieval. Returns a builder object.
     * @see org.easystub.CaptureReporter
     */
    public static CaptureReporter setupCaptureForLastInvocation() {
        Invocation last = GlobalInvocationReporter.lastInvocation();
        guardAgainstNoArgMethod(last.getMethodCalled());
        
        InvocationReporter reporter = stubToReporter.get(last.getObjectCalled());
        CaptureStore store = stubToCaptureStore.get(last.getObjectCalled());

        return new CaptureReporter(reporter, store);
    }

    private static void guardAgainstNoArgMethod(Method methodCalled) {
        if(methodCalled.getParameterTypes().length == 0) {
            throw new UnsupportedOperationException("Cannot register capturing on methods without arguments");
        }
    }

    /**
     * Allows recording of answers on a stub that may potentially throw exceptions when invocated.
     * Takes a callback where the stub can be safely invoked. Used when you create your stub with
     * {@link org.easystub.ReplyType#THROW_EXCEPTIONS} as the reply type.
     */
    public static <T> void setupStub(T stub, Setup<T> setup) {
        InvocationHandlerLocator locator = (InvocationHandlerLocator) stub;
        StubInvocationHandler invocationHandler = locator.getInvocationHandler();
        InvocationAnswerer stubDefault = invocationHandler.getDefaultAnswerer();
        invocationHandler.setDefaultAnswerer(new DefaultInvocationAnswerer());
        setup.usingStub(stub);
        invocationHandler.setDefaultAnswerer(stubDefault);
    }

    /**
     * Creates and returns a stub that by default will throw exceptions to all invocations with no reply.
     * Uses a callback where the stub answers can be safely setup.
     */
    public static <T> T setupStub(Class<T> classToStub, Setup<T> setup) {
        T stub = createStub(classToStub, ReplyType.THROW_EXCEPTIONS);
        setupStub(stub, setup);
        return stub;
    }
}
